home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / util.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  25.1 KB  |  751 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. import os
  19. import random
  20. import re
  21. import sys
  22. import sha
  23. import time
  24. import string
  25. import urllib
  26. import socket
  27. import logging
  28. import filetypes
  29. import tempfile
  30. import threading
  31. import traceback
  32. import subprocess
  33.  
  34. from clock import clock
  35. from types import UnicodeType, StringType
  36. from BitTorrent.bencode import bdecode, bencode
  37.  
  38. # Should we print out warning messages.  Turn off in the unit tests.
  39. chatter = True
  40.  
  41. inDownloader = False
  42. # this gets set to True when we're in the download process.
  43.  
  44. ignoreErrors = False
  45.  
  46. # Perform escapes needed for Javascript string contents.
  47. def quoteJS(x):
  48.     x = x.replace("\\", "\\\\") # \       -> \\
  49.     x = x.replace("\"", "\\\"") # "       -> \"  
  50.     x = x.replace("'",  "\\'")  # '       -> \'
  51.     x = x.replace("\n", "\\n")  # newline -> \n
  52.     x = x.replace("\r", "\\r")  # CR      -> \r
  53.     return x
  54.  
  55. def getNiceStack():
  56.     """Get a stack trace that's a easier to read that the full one.  """
  57.     stack = traceback.extract_stack()
  58.     # We don't care about the unit test lines
  59.     while (len(stack) > 0 and
  60.         os.path.basename(stack[0][0]) == 'unittest.py' or 
  61.         (isinstance(stack[0][3], str) and 
  62.             stack[0][3].startswith('unittest.main'))):
  63.         stack = stack[1:]
  64.     # remove after the call to util.failed
  65.     for i in xrange(len(stack)):
  66.         if (os.path.basename(stack[i][0]) == 'util.py' and 
  67.                 stack[i][2] in ('failed', 'failedExn')):
  68.             stack = stack[:i+1]
  69.             break
  70.     # remove trapCall calls
  71.     stack = [i for i in stack if i[2] != 'trapCall']
  72.     return stack
  73.  
  74. # Parse a configuration file in a very simple format. Each line is
  75. # either whitespace or "Key = Value". Whitespace is ignored at the
  76. # beginning of Value, but the remainder of the line is taken
  77. # literally, including any whitespace. There is no way to put a
  78. # newline in a value. Returns the result as a dict.
  79. def readSimpleConfigFile(path):
  80.     ret = {}
  81.  
  82.     f = open(path, "rt")
  83.     for line in f.readlines():
  84.         # Skip blank lines
  85.         if re.match("^[ \t]*$", line):
  86.             continue
  87.  
  88.         # Otherwise it'd better be a configuration setting
  89.         match = re.match(r"^([^ ]+) *= *([^\r\n]*)[\r\n]*$", line)
  90.         if not match:
  91.             print "WARNING: %s: ignored bad configuration directive '%s'" % (path, line)
  92.             continue
  93.         
  94.         key = match.group(1)
  95.         value = match.group(2)
  96.         if key in ret:
  97.             print "WARNING: %s: ignored duplicate directive '%s'" % (path, line)
  98.             continue
  99.  
  100.         ret[key] = value
  101.  
  102.     return ret
  103.  
  104. # Given a dict, write a configuration file in the format that
  105. # readSimpleConfigFile reads.
  106. def writeSimpleConfigFile(path, data):
  107.     f = open(path, "wt")
  108.  
  109.     for (k, v) in data.iteritems():
  110.         f.write("%s = %s\n" % (k, v))
  111.     
  112.     f.close()
  113.  
  114. # Called at build-time to ask Subversion for the revision number of
  115. # this checkout. Going to fail without Cygwin. Yeah, oh well. Pass the
  116. # file or directory you want to use as a reference point. Returns an
  117. # integer on success or None on failure.
  118. def queryRevision(f):
  119.     try:
  120.         p = subprocess.Popen(["svn", "info", f], stdout=subprocess.PIPE) 
  121.         info = p.stdout.read()
  122.         p.stdout.close()
  123.         url = re.search("URL: (.*)", info).group(1)
  124.         url = url.strip()
  125.         revision = re.search("Revision: (.*)", info).group(1)
  126.         revision = revision.strip()
  127.         return (url, revision)
  128.     except KeyboardInterrupt:
  129.         raise
  130.     except:
  131.         # whatever
  132.         return None
  133.  
  134. # 'path' is a path that could be passed to open() to open a file on
  135. # this platform. It must be an absolute path. Return the file:// URL
  136. # that would refer to the same file.
  137. def absolutePathToFileURL(path):
  138.     if isinstance(path, unicode):
  139.         path = path.encode("utf-8")
  140.     parts = string.split(path, os.sep)
  141.     parts = [urllib.quote(x, ':') for x in parts]
  142.     return "file://" + '/'.join(parts)
  143.  
  144.  
  145. # Shortcut for 'failed' with the exception flag.
  146. def failedExn(when, **kwargs):
  147.     failed(when, withExn = True, **kwargs)
  148.  
  149. # Puts up a dialog with debugging information encouraging the user to
  150. # file a ticket. (Also print a call trace to stderr or whatever, which
  151. # hopefully will end up on the console or in a log.) 'when' should be
  152. # something like "when trying to play a video." The user will see
  153. # it. If 'withExn' is true, last-exception information will be printed
  154. # to. If 'detail' is true, it will be included in the report and the
  155. # the console/log, but not presented in the dialog box flavor text.
  156. def failed(when, withExn = False, details = None):
  157.     logging.info ("failed() called; generating crash report.")
  158.  
  159.     header = ""
  160.     try:
  161.         import config # probably works at runtime only
  162.         import prefs
  163.         header += "App:        %s\n" % config.get(prefs.LONG_APP_NAME)
  164.         header += "Publisher:  %s\n" % config.get(prefs.PUBLISHER)
  165.         header += "Platform:   %s\n" % config.get(prefs.APP_PLATFORM)
  166.         header += "Python:     %s\n" % sys.version.replace("\r\n"," ").replace("\n"," ").replace("\r"," ")
  167.         header += "Py Path:    %s\n" % repr(sys.path)
  168.         header += "Version:    %s\n" % config.get(prefs.APP_VERSION)
  169.         header += "Serial:     %s\n" % config.get(prefs.APP_SERIAL)
  170.         header += "Revision:   %s\n" % config.get(prefs.APP_REVISION)
  171.         header += "Builder:    %s\n" % config.get(prefs.BUILD_MACHINE)
  172.         header += "Build Time: %s\n" % config.get(prefs.BUILD_TIME)
  173.     except KeyboardInterrupt:
  174.         raise
  175.     except:
  176.         pass
  177.     header += "Time:       %s\n" % time.asctime()
  178.     header += "When:       %s\n" % when
  179.     header += "\n"
  180.  
  181.     if withExn:
  182.         header += "Exception\n---------\n"
  183.         header += ''.join(traceback.format_exception(*sys.exc_info()))
  184.         header += "\n"
  185.     if details:
  186.         header += "Details: %s\n" % (details, )
  187.     header += "Call stack\n----------\n"
  188.     try:
  189.         stack = getNiceStack()
  190.     except KeyboardInterrupt:
  191.         raise
  192.     except:
  193.         stack = traceback.extract_stack()
  194.     header += ''.join(traceback.format_list(stack))
  195.     header += "\n"
  196.  
  197.     header += "Threads\n-------\n"
  198.     header += "Current: %s\n" % threading.currentThread().getName()
  199.     header += "Active:\n"
  200.     for t in threading.enumerate():
  201.         header += " - %s%s\n" % \
  202.             (t.getName(),
  203.              t.isDaemon() and ' [Daemon]' or '')
  204.  
  205.     # Combine the header with the logfile contents, if available, to
  206.     # make the dialog box crash message. {{{ and }}} are Trac
  207.     # Wiki-formatting markers that force a fixed-width font when the
  208.     # report is pasted into a ticket.
  209.     report = "{{{\n%s}}}\n" % header
  210.  
  211.     def readLog(logFile, logName="Log"):
  212.         try:
  213.             f = open(logFile, "rt")
  214.             logContents = "%s\n---\n" % logName
  215.             logContents += f.read()
  216.             f.close()
  217.         except KeyboardInterrupt:
  218.             raise
  219.         except:
  220.             logContents = ''
  221.         return logContents
  222.  
  223.     logFile = config.get(prefs.LOG_PATHNAME)
  224.     downloaderLogFile = config.get(prefs.DOWNLOADER_LOG_PATHNAME)
  225.     if logFile is None:
  226.         logContents = "No logfile available on this platform.\n"
  227.     else:
  228.         logContents = readLog(logFile)
  229.     if downloaderLogFile is not None:
  230.         if logContents is not None:
  231.             logContents += "\n" + readLog(downloaderLogFile, "Downloader Log")
  232.         else:
  233.             logContents = readLog(downloaderLogFile)
  234.  
  235.     if logContents is not None:
  236.         report += "{{{\n%s}}}\n" % stringify(logContents)
  237.  
  238.     # Dump the header for the report we just generated to the log, in
  239.     # case there are multiple failures or the user sends in the log
  240.     # instead of the report from the dialog box. (Note that we don't
  241.     # do this until we've already read the log into the dialog
  242.     # message.)
  243.     logging.info ("----- CRASH REPORT (DANGER CAN HAPPEN) -----")
  244.     logging.info (header)
  245.     logging.info ("----- END OF CRASH REPORT -----")
  246.  
  247.     if not inDownloader:
  248.         try:
  249.             import dialogs
  250.             from gtcache import gettext as _
  251.             if not ignoreErrors:
  252.                 chkboxdialog = dialogs.CheckboxTextboxDialog(_("Internal Error"),_("Miro has encountered an internal error. You can help us track down this problem and fix it by submitting an error report."), _("Include entire program database including all video and channel metadata with crash report"), False, _("Describe what you were doing that caused this error"), dialogs.BUTTON_SUBMIT_REPORT, dialogs.BUTTON_IGNORE)
  253.                 chkboxdialog.run(lambda x: _sendReport(report, x))
  254.         except Exception, e:
  255.             logging.exception ("Execption when reporting errror..")
  256.     else:
  257.         from dl_daemon import command, daemon
  258.         c = command.DownloaderErrorCommand(daemon.lastDaemon, report)
  259.         c.send()
  260.  
  261. def _sendReport(report, dialog):
  262.     def callback(result):
  263.         app.controller.sendingCrashReport -= 1
  264.         if result['status'] != 200 or result['body'] != 'OK':
  265.             logging.warning(u"Failed to submit crash report. Server returned %r" % result)
  266.         else:
  267.             logging.info(u"Crash report submitted successfully")
  268.     def errback(error):
  269.         app.controller.sendingCrashReport -= 1
  270.         logging.warning(u"Failed to submit crash report %r" % error)
  271.  
  272.     import dialogs
  273.     import httpclient
  274.     import config
  275.     import prefs
  276.     import app
  277.  
  278.     global ignoreErrors
  279.     if dialog.choice == dialogs.BUTTON_IGNORE:
  280.         ignoreErrors = True
  281.         return
  282.  
  283.     backupfile = None
  284.     if hasattr(dialog,"checkbox_value") and dialog.checkbox_value:
  285.         try:
  286.             logging.info("Sending entire database")
  287.             import database
  288.             backupfile = database.defaultDatabase.liveStorage.backupDatabase()
  289.         except:
  290.             traceback.print_exc()
  291.             logging.warning(u"Failed to backup database")
  292.  
  293.  
  294.     description = u"Description text not implemented"
  295.     if hasattr(dialog,"textbox_value"):
  296.         description = dialog.textbox_value
  297.  
  298.     description = description.encode("utf-8")
  299.     postVars = {"description":description,
  300.                 "app_name": config.get(prefs.LONG_APP_NAME),
  301.                 "log": report}
  302.     if backupfile:
  303.         postFiles = {"databasebackup": {"filename":"databasebackup.zip", "mimetype":"application/octet-stream", "handle":open(backupfile, "rb")}}
  304.     else:
  305.         postFiles = None
  306.     app.controller.sendingCrashReport += 1
  307.     httpclient.grabURL("http://participatoryculture.org/bogondeflector/index.php", callback, errback, method="POST", postVariables = postVars, postFiles = postFiles)
  308.  
  309. class AutoflushingStream:
  310.     """Converts a stream to an auto-flushing one.  It behaves in exactly the
  311.     same way, except all write() calls are automatically followed by a
  312.     flush().
  313.     """
  314.     def __init__(self, stream):
  315.         self.__dict__['stream'] = stream
  316.     def write(self, data):
  317.         if isinstance(data, unicode):
  318.             data = data.encode('ascii', 'backslashreplace')
  319.         self.stream.write(data)
  320.         self.stream.flush()
  321.     def __getattr__(self, name):
  322.         return getattr(self.stream, name)
  323.     def __setattr__(self, name, value):
  324.         return setattr(self.stream, name, value)
  325.  
  326. def makeDummySocketPair():
  327.     """Create a pair of sockets connected to each other on the local
  328.     interface.  Used to implement SocketHandler.wakeup().
  329.     """
  330.  
  331.     dummy_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  332.     dummy_server.bind( ('127.0.0.1', 0) )
  333.     dummy_server.listen(1)
  334.     server_address = dummy_server.getsockname()
  335.     first = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  336.     first.connect(server_address)
  337.     second, address = dummy_server.accept()
  338.     dummy_server.close()
  339.     return first, second
  340.  
  341. def trapCall(when, function, *args, **kwargs):
  342.     """Make a call to a function, but trap any exceptions and do a failedExn
  343.     call for them.  Return True if the function successfully completed, False
  344.     if it threw an exception
  345.     """
  346.  
  347.     try:
  348.         function(*args, **kwargs)
  349.         return True
  350.     except KeyboardInterrupt:
  351.         raise
  352.     except:
  353.         failedExn(when)
  354.         return False
  355.  
  356. # Turn the next flag on to track the cumulative time for each when argument to
  357. # timeTrapCall().  Don't do this for production builds though!  Since we never
  358. # clean up the entries in the cumulative dict, turning this on amounts to a
  359. # memory leak.
  360. TRACK_CUMULATIVE = False 
  361. cumulative = {}
  362. cancel = False
  363.  
  364. def timeTrapCall(when, function, *args, **kwargs):
  365.     global cancel
  366.     cancel = False
  367.     start = clock()
  368.     retval = trapCall (when, function, *args, **kwargs)
  369.     end = clock()
  370.     if cancel:
  371.         return retval
  372.     if end-start > 1.0:
  373.         logging.timing ("WARNING: %s too slow (%.3f secs)",
  374.             when, end-start)
  375.     if TRACK_CUMULATIVE:
  376.         try:
  377.             total = cumulative[when]
  378.         except KeyboardInterrupt:
  379.             raise
  380.         except:
  381.             total = 0
  382.         total += end - start
  383.         cumulative[when] = total
  384.         return retval
  385.         if total > 5.0:
  386.             logging.timing ("%s cumulative is too slow (%.3f secs)",
  387.                 when, total)
  388.             cumulative[when] = 0
  389.     cancel = True
  390.     return retval
  391.  
  392. def getTorrentInfoHash(path):
  393.     f = open(path, 'rb')
  394.     try:
  395.         data = f.read()
  396.         metainfo = bdecode(data)
  397.         infohash = sha.sha(bencode(metainfo['info'])).digest()
  398.         return infohash
  399.     finally:
  400.         f.close()
  401.  
  402. class ExponentialBackoffTracker:
  403.     """Utility class to track exponential backoffs."""
  404.     def __init__(self, baseDelay):
  405.         self.baseDelay = self.currentDelay = baseDelay
  406.     def nextDelay(self):
  407.         rv = self.currentDelay
  408.         self.currentDelay *= 2
  409.         return rv
  410.     def reset(self):
  411.         self.currentDelay = self.baseDelay
  412.  
  413.  
  414. # Gather movie files on the disk. Used by the startup dialog.
  415. def gatherVideos(path, progressCallback):
  416.     import item
  417.     import prefs
  418.     import config
  419.     import platformutils
  420.     keepGoing = True
  421.     parsed = 0
  422.     found = list()
  423.     try:
  424.         for root, dirs, files in os.walk(path):
  425.             for f in files:
  426.                 parsed = parsed + 1
  427.                 if filetypes.isVideoFilename(f):
  428.                     found.append(os.path.join(root, f))
  429.                 if parsed > 1000:
  430.                     adjustedParsed = int(parsed / 100.0) * 100
  431.                 elif parsed > 100:
  432.                     adjustedParsed = int(parsed / 10.0) * 10
  433.                 else:
  434.                     adjustedParsed = parsed
  435.                 keepGoing = progressCallback(adjustedParsed, len(found))
  436.                 if not keepGoing:
  437.                     found = None
  438.                     raise
  439.             if config.get(prefs.SHORT_APP_NAME) in dirs:
  440.                 dirs.remove(config.get(prefs.SHORT_APP_NAME))
  441.     except KeyboardInterrupt:
  442.         raise
  443.     except:
  444.         pass
  445.     return found
  446.  
  447. def formatSizeForUser(bytes, zeroString="", withDecimals=True, kbOnly=False):
  448.     """Format an int containing the number of bytes into a string suitable for
  449.     printing out to the user.  zeroString is the string to use if bytes == 0.
  450.     """
  451.     from gtcache import gettext as _
  452.     if bytes > (1 << 30) and not kbOnly:
  453.         value = (bytes / (1024.0 * 1024.0 * 1024.0))
  454.         if withDecimals:
  455.             format = _("%1.1fGB")
  456.         else:
  457.             format = _("%dGB")
  458.     elif bytes > (1 << 20) and not kbOnly:
  459.         value = (bytes / (1024.0 * 1024.0))
  460.         if withDecimals:
  461.             format = _("%1.1fMB")
  462.         else:
  463.             format = _("%dMB")
  464.     elif bytes > (1 << 10):
  465.         value = (bytes / 1024.0)
  466.         if withDecimals:
  467.             format = _("%1.1fKB")
  468.         else:
  469.             format = _("%dKB")
  470.     elif bytes > 1:
  471.         value = bytes
  472.         if withDecimals:
  473.             format = _("%1.1fB")
  474.         else:
  475.             format = _("%dB")
  476.     else:
  477.         return zeroString
  478.  
  479.     return format % value
  480.  
  481. def formatTimeForUser(seconds, sign=1):
  482.     """Format a duration in seconds into a string suitable for display, using
  483.     the minimum amount of digits. Negative durations used for remaining times
  484.     display a '-' sign.
  485.     """
  486.     _, _, _, h, m, s, _, _, _ = time.gmtime(seconds)
  487.     if sign < 0:
  488.         sign = '-'
  489.     else:
  490.         sign = ''
  491.     if int(seconds) in range(0, 3600):
  492.         return "%s%d:%02u" % (sign, m, s)
  493.     else:
  494.         return "%s%d:%02u:%02u" % (sign, h, m, s)
  495.  
  496. def makeAnchor(label, href):
  497.     return '<a href="%s">%s</a>' % (href, label)
  498.  
  499. def makeEventURL(label, eventURL):
  500.     return '<a href="#" onclick="return eventURL(\'action:%s\');">%s</a>' % \
  501.             (eventURL, label)
  502.  
  503. def clampText(text, maxLength):
  504.     if len(text) > maxLength:
  505.         return text[:maxLength-3] + '...'
  506.     else:
  507.         return text
  508.  
  509. def print_mem_usage(message):
  510.     pass
  511. # Uncomment for memory usage printouts on linux.
  512. #    print message
  513. #    os.system ("ps huwwwp %d" % (os.getpid(),))
  514.  
  515. class TooManySingletonsError(Exception):
  516.     pass
  517.  
  518. def getSingletonDDBObject(view):
  519.     view.confirmDBThread()
  520.     viewLength = view.len()
  521.     if viewLength == 1:
  522.         view.resetCursor()
  523.         return view.next()
  524.     elif viewLength == 0:
  525.         raise LookupError("Can't find singleton in %s" % repr(view))
  526.     else:
  527.         msg = "%d objects in %s" % (viewLength, len(view))
  528.         raise TooManySingletonsError(msg)
  529.  
  530. class ThreadSafeCounter:
  531.     """Implements a counter that can be access by multiple threads."""
  532.     def __init__(self, initialValue=0):
  533.         self.value = initialValue
  534.         self.lock = threading.Lock()
  535.  
  536.     def inc(self):
  537.         self.lock.acquire()
  538.         try:
  539.             self.value += 1
  540.         finally:
  541.             self.lock.release()
  542.  
  543.     def dec(self):
  544.         self.lock.acquire()
  545.         try:
  546.             self.value -= 1
  547.         finally:
  548.             self.lock.release()
  549.  
  550.     def getvalue(self):
  551.         self.lock.acquire()
  552.         try:
  553.             return self.value
  554.         finally:
  555.             self.lock.release()
  556.  
  557. def setupLogging():
  558.     logging.addLevelName(25, "TIMING")
  559.     logging.timing = lambda msg, *args, **kargs: logging.log(25, msg, *args, **kargs)
  560.     logging.addLevelName(26, "JSALERT")
  561.     logging.jsalert = lambda msg, *args, **kargs: logging.log(26, msg, *args, **kargs)
  562.  
  563.  
  564. # Returned when input to a template function isn't unicode
  565. class DemocracyUnicodeError(StandardError):
  566.     pass
  567.  
  568. # Raise an exception if input isn't unicode
  569. def checkU(text):
  570.     if text is not None and type(text) != UnicodeType:
  571.         raise DemocracyUnicodeError, (u"text \"%s\" is not a unicode string" %
  572.                                      text)
  573.  
  574. # Decorator that raised an exception if the function doesn't return unicode
  575. def returnsUnicode(func):
  576.     def checkFunc(*args, **kwargs):
  577.         result = func(*args,**kwargs)
  578.         if result is not None:
  579.             checkU(result)
  580.         return result
  581.     return checkFunc
  582.  
  583. # Raise an exception if input isn't a binary string
  584. def checkB(text):
  585.     if text is not None and type(text) != StringType:
  586.         raise DemocracyUnicodeError, (u"text \"%s\" is not a binary string" %
  587.                                      text)
  588.  
  589. # Decorator that raised an exception if the function doesn't return unicode
  590. def returnsBinary(func):
  591.     def checkFunc(*args, **kwargs):
  592.         result = func(*args,**kwargs)
  593.         if result is not None:
  594.             checkB(result)
  595.         return result
  596.     return checkFunc
  597.  
  598. # Raise an exception if input isn't a URL type
  599. def checkURL(text):
  600.     if type(text) != UnicodeType:
  601.         raise DemocracyUnicodeError, (u"url \"%s\" is not unicode" %
  602.                                      text)
  603.     try:
  604.         text.encode('ascii')
  605.     except:
  606.         raise DemocracyUnicodeError, (u"url \"%s\" contains extended characters" %
  607.                                      text)
  608.  
  609. # Decorator that raised an exception if the function doesn't return a filename
  610. def returnsURL(func):
  611.     def checkFunc(*args, **kwargs):
  612.         result = func(*args,**kwargs)
  613.         if result is not None:
  614.             checkURL(result)
  615.         return result
  616.     return checkFunc
  617.  
  618. # Returns exception if input isn't a filename type
  619. def checkF(text):
  620.     from platformutils import FilenameType
  621.     if text is not None and type(text) != FilenameType:
  622.         raise DemocracyUnicodeError, (u"text \"%s\" is not a valid filename type" %
  623.                                      text)
  624.  
  625. # Decorator that raised an exception if the function doesn't return a filename
  626. def returnsFilename(func):
  627.     def checkFunc(*args, **kwargs):
  628.         result = func(*args,**kwargs)
  629.         if result is not None:
  630.             checkF(result)
  631.         return result
  632.     return checkFunc
  633.  
  634. def unicodify(d):
  635.     """Turns all strings in data structure to unicode.
  636.     """
  637.     if isinstance(d, dict):
  638.         for key in d.keys():
  639.             d[key] = unicodify(d[key])
  640.     elif isinstance(d, list):
  641.         for key in range(len(d)):
  642.             d[key] = unicodify(d[key])
  643.     elif type(d) == StringType:
  644.         d = d.decode('ascii','replace')
  645.     return d
  646.  
  647. def stringify(u, handleerror="xmlcharrefreplace"):
  648.     """Takes a possibly unicode string and converts it to a string string.
  649.     This is required for some logging especially where the things being
  650.     logged are filenames which can be Unicode in the Windows platform.
  651.  
  652.     Note that this is not the inverse of unicodify.
  653.  
  654.     You can pass in a handleerror argument which defaults to "xmlcharrefreplace".
  655.     This will increase the string size as it converts unicode characters that
  656.     don't have ascii equivalents into escape sequences.  If you don't want to
  657.     increase the string length, use "replace" which will use ? for unicode
  658.     characters that don't have ascii equivalents.
  659.     """
  660.     if isinstance(u, unicode):
  661.         return u.encode("ascii", handleerror)
  662.     if not isinstance(u, str):
  663.         return str(u)
  664.     return u
  665.  
  666. def quoteUnicodeURL(url):
  667.     """Quote international characters contained in a URL according to w3c, see:
  668.     <http://www.w3.org/International/O-URL-code.html>
  669.     """
  670.     checkU(url)
  671.     quotedChars = list()
  672.     for c in url.encode('utf8'):
  673.         if ord(c) > 127:
  674.             quotedChars.append(urllib.quote(c))
  675.         else:
  676.             quotedChars.append(c)
  677.     return u''.join(quotedChars)
  678.  
  679. def no_console_startupinfo():
  680.     """Returns the startupinfo argument for subprocess.Popen so that we don't
  681.     open a console window.  On platforms other than windows, this is just
  682.     None.  On windows, it's some win32 sillyness.
  683.     """
  684.     if subprocess.mswindows:
  685.         startupinfo = subprocess.STARTUPINFO()
  686.         startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  687.         return startupinfo
  688.     else:
  689.         return None
  690.  
  691. def call_command(*args, **kwargs):
  692.     """Call an external command.  If the command doesn't exit with status 0,
  693.     or if it outputs to stderr, an exception will be raised.  Returns stdout.
  694.     """
  695.     ignore_stderr = kwargs.pop('ignore_stderr', False)
  696.     if kwargs:
  697.         raise TypeError('extra keyword arguments: %s' % kwargs)
  698.  
  699.     pipe = subprocess.Popen(args, stdout=subprocess.PIPE,
  700.             stdin=subprocess.PIPE, stderr=subprocess.PIPE,
  701.             startupinfo=no_console_startupinfo())
  702.     stdout, stderr = pipe.communicate()
  703.     if pipe.returncode != 0:
  704.         raise OSError("call_command with %s has return code %s\nstdout:%s\nstderr:%s" % 
  705.                 (args, pipe.returncode, stdout, stderr))
  706.     elif stderr and not ignore_stderr:
  707.         raise OSError("call_command with %s outputed error text:\n%s" % 
  708.                 (args, stderr))
  709.     else:
  710.         return stdout
  711.  
  712. def getsize(path):
  713.     """Get the size of a path.  If it's a file, return the size of the file.
  714.     If it's a directory return the total size of all the files it contains.
  715.     """
  716.  
  717.     if os.path.isdir(path):
  718.         size = 0
  719.         for (dirpath, dirnames, filenames) in os.walk(path):
  720.             for name in filenames:
  721.                 size += os.path.getsize(os.path.join(dirpath, name))
  722.             size += os.path.getsize(dirpath)
  723.         return size
  724.     else:
  725.         return os.path.getsize(path)
  726.  
  727. def partition(list, size):
  728.     """Partiction list into smaller lists such that none is larger than
  729.     size elements.
  730.  
  731.     Returns a list of lists.  The lists appended together will be the original
  732.     list.
  733.     """
  734.     retval = []
  735.     for start in range(0, len(list), size):
  736.         retval.append(list[start:start+size])
  737.     return retval
  738.  
  739. def directoryWritable(directory):
  740.     """Check if we can write to a directory."""
  741.     try:
  742.         f = tempfile.TemporaryFile(dir=directory)
  743.     except OSError:
  744.         return False
  745.     else:
  746.         f.close()
  747.         return True
  748.  
  749. def random_string(length):
  750.     return ''.join(random.choice(string.ascii_letters) for i in xrange(length))
  751.